//#########################################################################
//                              WackiEngie
//           Copyright (c) 2014 Marc Wacker. All rights reserved.
//#########################################################################

#include "GLFW/WeGLFWPlatform.h"
#include "WeInputTypes.h"
#include "WeDebug.h"

// only include the glfw headers here
#include <glew.h>
#include <glfw3.h>

#define MOUSE_DOUBLE_CLICK_TIME 0.5

namespace WackiEngine
{
	// glfw global variables only visible to the implementation
	// TODO: better platform implementation
	GLFWwindow* mainWindow;

	const KeyCode getWEKeyCode(int32 key)
	{
		switch(key) {
		case GLFW_KEY_UNKNOWN:			return KC_Undefined;
		case GLFW_KEY_SPACE:			return KC_Space;
		case GLFW_KEY_APOSTROPHE:		return KC_Apostrophe; /* ' */
		case GLFW_KEY_COMMA:			return KC_Comma; /* , */
		case GLFW_KEY_MINUS:			return KC_Minus; /* - */
		case GLFW_KEY_PERIOD:			return KC_Period; /* . */
		case GLFW_KEY_SLASH:			return KC_Slash; /* / */
		case GLFW_KEY_0:				return KC_0;
		case GLFW_KEY_1:				return KC_1;
		case GLFW_KEY_2:				return KC_2;
		case GLFW_KEY_3:				return KC_3;
		case GLFW_KEY_4:				return KC_4;
		case GLFW_KEY_5:				return KC_5;
		case GLFW_KEY_6:				return KC_6;
		case GLFW_KEY_7:				return KC_7;
		case GLFW_KEY_8:				return KC_8;
		case GLFW_KEY_9:				return KC_9;
		case GLFW_KEY_SEMICOLON:		return KC_Semicolon; /* ; */
		case GLFW_KEY_EQUAL:			return KC_Equals; /* = */
		case GLFW_KEY_A:				return KC_A;
		case GLFW_KEY_B:				return KC_B;
		case GLFW_KEY_C:				return KC_C;
		case GLFW_KEY_D:				return KC_D;
		case GLFW_KEY_E:				return KC_E;
		case GLFW_KEY_F:				return KC_F;
		case GLFW_KEY_G:				return KC_G;
		case GLFW_KEY_H:				return KC_H;
		case GLFW_KEY_I:				return KC_I;
		case GLFW_KEY_J:				return KC_J;
		case GLFW_KEY_K:				return KC_K;
		case GLFW_KEY_L:				return KC_L;
		case GLFW_KEY_M:				return KC_M;
		case GLFW_KEY_N:				return KC_N;
		case GLFW_KEY_O:				return KC_O;
		case GLFW_KEY_P:				return KC_P;
		case GLFW_KEY_Q:				return KC_Q;
		case GLFW_KEY_R:				return KC_R;
		case GLFW_KEY_S:				return KC_S;
		case GLFW_KEY_T:				return KC_T;
		case GLFW_KEY_U:				return KC_U;
		case GLFW_KEY_V:				return KC_V;
		case GLFW_KEY_W:				return KC_W;
		case GLFW_KEY_X:				return KC_X;
		case GLFW_KEY_Y:				return KC_Y;
		case GLFW_KEY_Z:				return KC_Z;
		case GLFW_KEY_LEFT_BRACKET:		return KC_LeftBracket; /* [ */
		case GLFW_KEY_BACKSLASH:		return KC_Backslash; /* \ */
		case GLFW_KEY_RIGHT_BRACKET:	return KC_RightBracket; /* ] */
		case GLFW_KEY_GRAVE_ACCENT:		return KC_BackQuote; /* ` */
		case GLFW_KEY_WORLD_1:			return KC_Undefined; /* non-US #1 */
		case GLFW_KEY_WORLD_2:			return KC_Undefined; /* non-US #2 */
		case GLFW_KEY_ESCAPE:			return KC_Escape;
		case GLFW_KEY_ENTER:			return KC_Enter;
		case GLFW_KEY_TAB:				return KC_Tab;
		case GLFW_KEY_BACKSPACE:		return KC_Backspace;
		case GLFW_KEY_INSERT:			return KC_Insert;
		case GLFW_KEY_DELETE:			return KC_Delete;
		case GLFW_KEY_RIGHT:			return KC_RightArrow;
		case GLFW_KEY_LEFT:				return KC_LeftArrow;
		case GLFW_KEY_DOWN:				return KC_DownArrow;
		case GLFW_KEY_UP:				return KC_UpArrow;
		case GLFW_KEY_PAGE_UP:			return KC_PageUp;
		case GLFW_KEY_PAGE_DOWN:		return KC_PageDown;
		case GLFW_KEY_HOME:				return KC_Home;
		case GLFW_KEY_END:				return KC_End;
		case GLFW_KEY_CAPS_LOCK:		return KC_CapsLock;
		case GLFW_KEY_SCROLL_LOCK:		return KC_ScrollLock;
		case GLFW_KEY_NUM_LOCK:			return KC_Numlock;
		case GLFW_KEY_PRINT_SCREEN:		return KC_Print;
		case GLFW_KEY_PAUSE:			return KC_Pause;
		case GLFW_KEY_F1:				return KC_F1;
		case GLFW_KEY_F2:				return KC_F2;
		case GLFW_KEY_F3:				return KC_F3;
		case GLFW_KEY_F4:				return KC_F4;
		case GLFW_KEY_F5:				return KC_F5;
		case GLFW_KEY_F6:				return KC_F6;
		case GLFW_KEY_F7:				return KC_F7;
		case GLFW_KEY_F8:				return KC_F8;
		case GLFW_KEY_F9:				return KC_F9;
		case GLFW_KEY_F10:				return KC_F10;
		case GLFW_KEY_F11:				return KC_F11;
		case GLFW_KEY_F12:				return KC_F12;
		case GLFW_KEY_F13:				return KC_F13;
		case GLFW_KEY_F14:				return KC_F14;
		case GLFW_KEY_F15:				return KC_F15;
		case GLFW_KEY_KP_0:				return KC_Numpad0;
		case GLFW_KEY_KP_1:				return KC_Numpad1;
		case GLFW_KEY_KP_2:				return KC_Numpad2;
		case GLFW_KEY_KP_3:				return KC_Numpad3;
		case GLFW_KEY_KP_4:				return KC_Numpad4;
		case GLFW_KEY_KP_5:				return KC_Numpad5;
		case GLFW_KEY_KP_6:				return KC_Numpad6;
		case GLFW_KEY_KP_7:				return KC_Numpad7;
		case GLFW_KEY_KP_8:				return KC_Numpad8;
		case GLFW_KEY_KP_9:				return KC_Numpad9;
		case GLFW_KEY_KP_DECIMAL:		return KC_NumpadPeriod;
		case GLFW_KEY_KP_DIVIDE:		return KC_NumpadDivide;
		case GLFW_KEY_KP_MULTIPLY:		return KC_NumpadMultiply;
		case GLFW_KEY_KP_SUBTRACT:		return KC_NumpadMinus;
		case GLFW_KEY_KP_ADD:			return KC_NumpadPlus;
		case GLFW_KEY_KP_ENTER:			return KC_NumpadEnter;
		case GLFW_KEY_KP_EQUAL:			return KC_NumpadEquals;
		case GLFW_KEY_LEFT_SHIFT:		return KC_LeftShift;
		case GLFW_KEY_LEFT_CONTROL:		return KC_LeftControl;
		case GLFW_KEY_LEFT_ALT:			return KC_LeftAlt;
		case GLFW_KEY_LEFT_SUPER:		return KC_LeftCommand;
		case GLFW_KEY_RIGHT_SHIFT:		return KC_RightShift;
		case GLFW_KEY_RIGHT_CONTROL:	return KC_RightControl;
		case GLFW_KEY_RIGHT_ALT:		return KC_RightAlt;
		case GLFW_KEY_RIGHT_SUPER:		return KC_RightCommand;
		case GLFW_KEY_MENU:				return KC_Menu;
		default:						return KC_Undefined;
		}

	}

	KeyCode glfwMouseButtonToWE(int button)
	{
		switch(button) {
		case 0: return KC_Mouse_Left;
		case 1: return KC_Mouse_Right;
		case 2: return KC_Mouse_Middle;
		case 3: return KC_Mouse_Btn4;
		case 4: return KC_Mouse_Btn5;
		case 5: return KC_Mouse_Btn6;
		default: return KC_Mouse_Left;
		}
	}

	Platform::Platform(int32 width, int32 height, const String& title)
	{
		_width = width;
		_height = height;

		// initialize glfw
		if(!glfwInit())
			throw "glfw could'nt be initialized"; // TODO: add better exception handling, maybe move the glfw init somewhere else

		//Create a windowed mode window and its OpenGL context
		mainWindow = glfwCreateWindow(width, height, title.c_str(), NULL, NULL);

		if(!mainWindow) {
			glfwTerminate();
			throw "Main window couldn't be created correctly"; // TODO: better esception handling
		}
		
		//Make main window's context current
		glfwMakeContextCurrent(mainWindow);

		//temp enable multisampling
		glfwWindowHint(GLFW_SAMPLES, 4);

		//init glew after creating context
		GLenum res = glewInit();
		if(res != GLEW_OK) {
			throw "glew couldn't be initialized!"; // TODO: better exception handling
		}


		//bind callbacks (lambdas to use the member functions of the Platform singleton class!)
		glfwSetWindowSizeCallback(mainWindow, [](GLFWwindow* window, int w, int h) { gPlatform().onResize(w, h); });
		glfwSetWindowCloseCallback(mainWindow, [](GLFWwindow* window) { gPlatform().onWindowClosed(); });
		glfwSetMouseButtonCallback(mainWindow, 	[](GLFWwindow* window, int button, int action, int mods) { (action == GLFW_PRESS) ? gPlatform().onMouseDown(button) : gPlatform().onMouseUp(button); } );
		glfwSetCursorPosCallback(mainWindow, [](GLFWwindow* window, double x, double y) { gPlatform().onMouseMove((float)x, (float)y); });
		glfwSetScrollCallback(mainWindow, [](GLFWwindow* window, double x, double y) { gPlatform().onMouseWheel((float)x, (float)y); });
		glfwSetKeyCallback(mainWindow, [](GLFWwindow* window, int glfwKey, int scancode, int action, int mods) { 
			if(action == GLFW_RELEASE)
				gPlatform().onKeyboardUp(getWEKeyCode(glfwKey));
			else if(action == GLFW_PRESS)
				gPlatform().onKeyboardDown(getWEKeyCode(glfwKey));
		} );
		glfwSetCharCallback(mainWindow, [](GLFWwindow* window, unsigned int character) { gPlatform().onCharInput(character); });
		glfwSetCursorEnterCallback(mainWindow, [](GLFWwindow* window, int entered) {
			if(entered == GL_TRUE)
				gPlatform().onMouseWindowEnter();
			else
				gPlatform().onMouseWindowLeave();
		});


		/*-------------------------------------/
			finish the os callbacks
			- find a way to keep track of mouse pos and use that in the mouse callbacks
			- in the input class relay the osCallbacks to onKeyDown and onAxisMoved to keep track of deviceId = 0 (mouse and keyboard)
			- in InputDevice add a static variable to keep track of current deviceId
			- the constructor of InputDevices takes number of devices controlled by this inputDevicehandler(starts with 1) so Xinput will receive 1 - 4 as indices

			in the input class provide only one key callback
			- this function should fire on different event types(Pressed, Released, DoubleClicked, Repeat) and carry that information onwards
			- in the action mapper when you bind to an action you can specify what event type you want to be triggered on
			->action->bind(Pressed | Repeat, &onJump) // jump function is called on actionDown and as long as key is pressed



			test the new input setup by connecting some functions in the main.cpp and outputting info

			add log class

			*/

	}

	Platform::~Platform()
	{
		glfwTerminate();
	}

	void Platform::setTitle(const String& title)
	{
		glfwSetWindowTitle(mainWindow, title.c_str());
	}

	void Platform::update()
	{
		glfwPollEvents();
	}

	void Platform::swapBuffers()
	{
		glfwSwapBuffers(mainWindow);
	}


	void Platform::onMouseMove(float x, float y)
	{
		if(!mouseMove.empty())
		{
			PointerEvent e(x, y, x - _mouseX, y - _mouseY, KC_Undefined, _modifiers);

			mouseMove(e);
		}

		_mouseX = x;
		_mouseY = y;
	}

	void Platform::onMouseDown(int button)
	{
		KeyCode mouseBtn = glfwMouseButtonToWE(button);

		if(!mouseDown.empty())
		{
			PointerEvent e(_mouseX, _mouseY, 0.0f, 0.0f, mouseBtn, _modifiers);

			mouseDown(e);
		}


		// check for double clicks on this button
		if(!mouseDoubleClick.empty())
		{
			if(_lastMouseDown.find(mouseBtn) == _lastMouseDown.end())
				_lastMouseDown[mouseBtn] = 0.0;

			double delta = glfwGetTime() - _lastMouseDown[mouseBtn];

			if(delta < MOUSE_DOUBLE_CLICK_TIME)
			{
				PointerEvent e(_mouseX, _mouseY, 0.0f, 0.0f, mouseBtn, _modifiers);

				mouseDoubleClick(e);
			}

		}

		_lastMouseDown[mouseBtn] = glfwGetTime();

	}

	void Platform::onMouseUp(int button)
	{
		if(!mouseUp.empty())
		{
			PointerEvent e(_mouseX, _mouseY, 0.0f, 0.0f, glfwMouseButtonToWE(button), _modifiers);
			mouseUp(e);
		}
	}

	void Platform::onMouseWheel(float x, float y)
	{
		PointerEvent e(_mouseX, _mouseY, x, y, KC_Undefined, _modifiers);

		if(!mouseWheel.empty())
			mouseWheel(e);
	}

	void Platform::onKeyboardDown(KeyCode key)
	{
		// update modifier flags
		switch(key) {
		case KC_LeftShift:
		case KC_RightShift:
			_modifiers |= Mod_Shift;
			break;
		case KC_LeftControl:
		case KC_RightControl:
			_modifiers |= Mod_Ctrl;
			break;
		case KC_LeftAlt:
		case KC_RightAlt:
			_modifiers |= Mod_Alt;
			break;
		}

		if(!keyboardDown.empty())
			keyboardDown(key);
	}

	void Platform::onKeyboardUp(KeyCode key)
	{
		// update modifier flags
		switch(key) {
		case KC_LeftShift:
		case KC_RightShift:
			_modifiers &= ~Mod_Shift;
			break;
		case KC_LeftControl:
		case KC_RightControl:
			_modifiers &= ~Mod_Ctrl;
			break;
		case KC_LeftAlt:
		case KC_RightAlt:
			_modifiers &= ~Mod_Alt;
			break;
		}

		if(!keyboardUp.empty())
			keyboardUp(key);
	}

	void Platform::onCharInput(uint32 character)
	{
		if(!charInput.empty())
			charInput(character);
	}

	void Platform::onResize(int32 w, int32 h)
	{
		if(!resize.empty())
			resize(w, h);
	}
	void Platform::onMouseWindowEnter()
	{
		if(!mouseWindowEnter.empty())
			mouseWindowEnter();
	}
	void Platform::onMouseWindowLeave()
	{
		if(!mouseWindowLeave.empty())
			mouseWindowLeave();
	}

	void Platform::onWindowClosed()
	{
		if(!windowClosed.empty())
			windowClosed();
	}




	Platform& gPlatform() 
	{
		return Platform::instance();
	}

}